#include <GameEngine/Core/Files/FileIO.h>


#ifdef GAMEENGINE_NAMESPACES
namespace GameEngine {
#endif

GAMEENGINE_IMPLEMENT_RTTI(FileIO)


//! Constructor
/*!
 @param filename The path to the file holding the data
 @param mode The open mode of the file
 */
FileIO::FileIO(const GEString& filename, IOMode mode) :
	file_(nil), mode_(mode), filename_(filename), file_position_(0),
	open_func_(&FileIO::internal_open),
	read_func_(&FileIO::internal_read_element_file_error),
	write_func_(&FileIO::internal_write_element_file_error) {}

//! Destructor
/*!
 *  Close the file if it was opened
 */
FileIO::~FileIO() {
	close_file();
}

//! Read a string from the file
bool FileIO::read_string(char* str, int max_length) {
	return ( fgets(str, max_length, file_) != nil );
}

//! Write a string to the file
bool FileIO::write_string(char* str) {
	return ( fputs(str, file_) != EOF );
}

//! Read one or several elements from the file
/*!
 @param element The elements where read value should go
 @param nb_bits The size of a single element in bits
 @param nb_elements The number of elements
 @return True if element was succesfully read, false otherwise
 */
bool FileIO::read_element(void* element, BitSize nb_bits, ElementCount nb_elements) {
	// Open file if it is not already opened
	((*this).*open_func_)();

	// Read the element from the file
	return ((*this).*read_func_)(element, nb_bits, nb_elements);
}

//! Write one or several element to the file
/*!
 @param element The elements to write
 @param nb_bits The size of a single element in bits
 @param nb_elements The number of elements
 @return True if element was succesfully written, false otherwise
 */
bool FileIO::write_element(const void* element, BitSize nb_bits, ElementCount nb_elements) {
	// Open file if it is not already opened
	((*this).*open_func_)();

	// Read the element from the file
	return ((*this).*write_func_)(element, nb_bits, nb_elements);
}

//! Internal function which actually opens the file
/*!
 @note If the file was previously released and last
       file position was not zero, this function
	   seeks to the last file position
 */
void FileIO::internal_open() {
	// Try to open the file
	fopen_s(&file_, filename_.c_str(), FileIO::code(mode_).c_str());

	// Update function pointers
	if ( file_ == nil ) {
		read_func_ = &FileIO::internal_read_element_file_error;
		write_func_ = &FileIO::internal_write_element_file_error;
	}
	else {
		// Check if file is readable
		if ( FileIO::read(mode_) ) read_func_ = &FileIO::internal_read_element_file_ok;
		else read_func_ = &FileIO::internal_read_element_file_error;

		// Check if file is writable
		if ( FileIO::write(mode_) ) write_func_ = &FileIO::internal_write_element_file_ok;
		else write_func_ = &FileIO::internal_write_element_file_error;

		// Seek previous file position
		if ( file_position_ != 0 ) {
			fseek(file_, file_position_, SEEK_SET);
		}
	}
	open_func_ = &FileIO::internal_opened;
}

//! Dummy internal function called when the file is already opened
/*!
 @note Does nothing
 */
void FileIO::internal_opened() {}

//! Read an element at the current position in the file
/*!
 @param element The item where read results are stored
 @param nb_bits The size of a single element in bits
 @param nb_elements The number of elements
 @return True if read was successful, false otherwise
 @note The elements are read from the current position in the file
 */
bool FileIO::internal_read_element_file_ok(void* element, BitSize nb_bits, ElementCount nb_elements) {
	size_t tmp = fread(element, nb_bits, nb_elements, file_);
	return ( tmp == nb_elements );
}

//! Dummy internal function called when the file is unreadable
/*!
 @param element The item where read results should be stored. As
                the file is not readable, this value is actually
				never modified
 @param nb_bits The size of a single element in bits. As the file is
                not readable, this value is actually never used
 @param nb_elements The number of elements. As the file is
                not readable, this value is actually never used
 @return Always return false
 */
bool FileIO::internal_read_element_file_error(void* element, BitSize nb_bits, ElementCount nb_elements) {
	return false;
}

//! Write an element at the current position in the file
/*!
 @param element The items to write
 @param nb_bits The size of a single element in bits
 @param nb_elements The number of elements
 @return True if write was successful, false otherwise
 @note The elements are written at the current position in the file
 */
bool FileIO::internal_write_element_file_ok(const void* element, BitSize nb_bits, ElementCount nb_elements) {
	size_t tmp = fwrite(element, nb_bits, nb_elements, file_);
	return ( tmp == nb_elements );
}

//! Dummy internal function called when the file is unwritable
/*!
 @param element The items to write. As the file is not writable,
				this value is actually never modified
 @param nb_bits The size of a single element in bits. As the file is
                not writable, this value is actually never used
 @param nb_elements The number of elements. As the file is
                not writable, this value is actually never used
 @return Always return false
 */
bool FileIO::internal_write_element_file_error(const void* element, BitSize nb_bits, ElementCount nb_elements) {
	return false;
}

//! Open the file
/*!
 @return True if file was opened, false otherwise
 */
bool FileIO::open_file() {
	// Open file
	((*this).*open_func_)();

	// Return result
	return opened();
}

//! Release the file
/*!
 @return True if file was released, false otherwise
 @note Close the file so that other file handlers can
       open it. The file position is saved. Next time
	   the file will be opened by the reader, it will
	   seek to that position. Note that the reader will
	   not be aware of any change made to the file
	   while it was closed.
 */
bool FileIO::release_file() {
	file_position_ = ( file_ == nil ? file_position_ : ftell(file_) );
	open_func_ = &FileIO::internal_open;
	read_func_ = &FileIO::internal_read_element_file_error;
	write_func_ = &FileIO::internal_write_element_file_error;
	return ( file_ == nil ? false : ( fclose(file_) == 0 ) );
}

//! Close the file
/*!
 @return True if file was closed, false otherwise
 @note Close the file so that other file handlers can
       open it. The file position is reset to zero. Next
	   time the file will be opened by the reader, its
	   position will be the beginning of the file
 */
bool FileIO::close_file() {
	file_position_ = 0;
	open_func_ = &FileIO::internal_open;
	read_func_ = &FileIO::internal_read_element_file_error;
	write_func_ = &FileIO::internal_write_element_file_error;
	return ( file_ == nil ? false : ( fclose(file_) == 0 ) );
}

//! Check if the file is currently opened or not
/*!
 @return True if the file is opened, false otherwise
 */
bool FileIO::opened() const {
	return (
		( read_func_ == &FileIO::internal_read_element_file_ok ) ||
		( write_func_ == &FileIO::internal_write_element_file_ok )
	);
}

//! Get the file position
/*!
 @return The file position
 @note If file is opened, this function reads the actual
	   position. If it is closed, the function returns
	   the last file position.
 */
FileIO::FilePosition FileIO::position() const {
	if ( opened() ) return ( file_ == nil ? file_position_ : ftell(file_) );
	return file_position_;
}

//! Change the file position
/*!
 @param position The new position in the file
 @return True if file position was succesfully changed,
         false otherwise. If file is not opened, this
		 function always returns true.
 @note If the file is not opened, this function simply
       changes the stored position. If it is not changed
	   until the next opening of the file, the open
	   function will seek to that position
 */
bool FileIO::set_position(FilePosition position) {
	file_position_ = position;
	return ( file_ == nil ? true : ( fseek(file_, file_position_, SEEK_SET) == 0 ) );
}




//! Check if given mode is allowed to read a file
/*!
 @param mode The input/output mode of a file
 @return True if mode allows reading operations, false otherwise
 */
bool FileIO::read(IOMode mode) {
	return ( ( (int)mode % 2 ) >= 1 );
}

//! Check if given mode is allowed to write a file
/*!
 @param mode The input/output mode of a file
 @return True if mode allows writing operations, false otherwise
 */
bool FileIO::write(IOMode mode) {
	return ( ( (int)mode % 4 ) >= 2 );
}

//! Check if given mode reads/write a file as binary
/*!
 @param mode The input/output mode of a file
 @return True if mode reads/write a file as binary, false otherwise
 */
bool FileIO::binary(IOMode mode) {
	return ( ( (int)mode % 8 ) >= 4 );
}

//! Check if given mode requires a file to exist
/*!
 @param mode The input/output mode of a file
 @return True if mode requires a file to exist, false otherwise
 */
bool FileIO::must_exist(IOMode mode) {
	return ( ( (int)mode % 16 ) >= 8 );
}

//! Check if given mode overwrites existing files
/*!
 @param mode The input/output mode of a file
 @return True if mode overwrites existing files, false otherwise
 */
bool FileIO::overwrite(IOMode mode) {
	return ( ( (int)mode % 32 ) >= 16 );
}

//! Check if given mode appends data at the end of files
/*!
 @param mode The input/output mode of a file
 @return True if mode appends data at the end of files, false otherwise
 */
bool FileIO::append(IOMode mode) {
	return ( (int)mode >= 32 );
}

//! Build the C code representing the mode
/*!
 @param mode The input/output mode of a file
 @return The C string which represents the input/output mode
 */
GEString FileIO::code(IOMode mode) {
	// Create result string
	GEString res;

	// Find read/write/append mode
	if ( FileIO::must_exist(mode) ) res += 'r';
	else if ( FileIO::overwrite(mode) ) res += 'w';
	else res += 'a';

	// Find read + write mode
	if ( FileIO::read(mode) ) {
		if ( FileIO::write(mode) ) res += '+';
	}

	// Find binary/text mode
	if ( FileIO::binary(mode) ) res += 'b';
	else res += 't';

	// Return result string
	return res;
}

#ifdef GAMEENGINE_NAMESPACES
} // namespace GameEngine
#endif
